BidaAngMamayan: verified-publisher gate + In Pictures + standalone board band
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.
This commit is contained in:
parent
7a3c9ec7c5
commit
70a23bbd46
114
app/globals.css
114
app/globals.css
@ -667,4 +667,118 @@ footer.site .disc { max-width: 640px; margin: 0 auto 10px; line-height: 1.5; }
|
||||
animation: shimmer 1.4s infinite;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* ── Full-width content bands (In Pictures, The Board) ──────────────────
|
||||
These are deliberately NOT news desks: they sit above / below the wire
|
||||
grid in their own gold-edged bands so a visitor immediately sees them. */
|
||||
.band {
|
||||
margin: 22px 0;
|
||||
background:
|
||||
radial-gradient(120% 200% at 0% 0%, rgba(255, 212, 38, 0.05), transparent 45%),
|
||||
var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-top: 2px solid var(--gold-dim);
|
||||
border-radius: 10px;
|
||||
padding: 18px 20px 22px;
|
||||
box-shadow: var(--glow-soft);
|
||||
}
|
||||
.bandhead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 12px; margin-bottom: 14px; }
|
||||
.bandtitle {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 800;
|
||||
color: var(--gold);
|
||||
font-size: 20px;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.bandsub { color: var(--muted); font-size: 14px; font-weight: 500; letter-spacing: 0.02em; }
|
||||
|
||||
/* The board button is NOT a news desk — give it its own gold treatment so it
|
||||
reads as "another area of the site", not a category. */
|
||||
.navbtn.board {
|
||||
background: var(--gold);
|
||||
color: #14110b;
|
||||
border-color: var(--gold);
|
||||
font-weight: 700;
|
||||
}
|
||||
.navbtn.board .dot { background: #14110b; }
|
||||
.navbtn.board:hover { background: var(--gold-2); box-shadow: var(--glow); }
|
||||
.navbtn.board.active { background: var(--gold-2); box-shadow: var(--glow); }
|
||||
|
||||
/* In Pictures */
|
||||
.photoscroll {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.photo {
|
||||
margin: 0;
|
||||
background: var(--bg-2);
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s;
|
||||
}
|
||||
.photo:hover { border-color: var(--gold-dim); box-shadow: var(--glow-soft); transform: translateY(-2px); }
|
||||
.photo .phimg {
|
||||
aspect-ratio: 4 / 3;
|
||||
background: var(--bg-2) center/cover no-repeat;
|
||||
}
|
||||
.photo figcaption { padding: 9px 11px 11px; }
|
||||
.photo .cap {
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.photo .cred {
|
||||
margin-top: 7px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--gold);
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.photo .lic {
|
||||
margin-top: 3px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--faint);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.photos-note {
|
||||
margin-top: 12px;
|
||||
color: var(--faint);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* The Board band */
|
||||
.board-band .postform.big { max-width: 720px; }
|
||||
.board-band .postform.big { display: grid; grid-template-columns: 1fr 2fr; gap: 10px 14px; align-items: end; }
|
||||
.board-band .postform.big label[for="bnick"] { grid-column: 1; }
|
||||
.board-band .postform.big #bnick { grid-column: 1; }
|
||||
.board-band .postform.big label[for="bmsg"] { grid-column: 2; }
|
||||
.board-band .postform.big #bmsg { grid-column: 2; }
|
||||
.board-band .postform.big .go { grid-column: 2; justify-self: start; }
|
||||
.board-band .postform.big .err { grid-column: 1 / -1; }
|
||||
.postlist.board { max-width: none; margin: 16px auto 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }
|
||||
.postlist.board .post { margin-bottom: 0; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.board-band .postform.big { grid-template-columns: 1fr; }
|
||||
.board-band .postform.big label[for="bnick"], .board-band .postform.big #bnick,
|
||||
.board-band .postform.big label[for="bmsg"], .board-band .postform.big #bmsg,
|
||||
.board-band .postform.big .go { grid-column: 1; }
|
||||
}
|
||||
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
|
||||
|
||||
74
app/page.js
74
app/page.js
@ -3,6 +3,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { CATEGORIES, sectionName } from "../lib/categories.js";
|
||||
import { clipText, dek } from "../lib/text.js";
|
||||
import { PHOTOS, commonsUrl } from "../lib/photos.js";
|
||||
|
||||
// BidaAngMamamayan — a PUBLIC tribute news desk for Senator BAM Aquino.
|
||||
// No login, no accounts, no "I like this" — the site's whole identity is the
|
||||
@ -200,7 +201,7 @@ export default function Home() {
|
||||
{c.label}
|
||||
</a>
|
||||
))}
|
||||
<a className={`navbtn ${view === "board" ? "active" : ""}`} href="#board" onClick={(e) => { e.preventDefault(); setView("board"); window.scrollTo({ top: 0 }); }}>
|
||||
<a className={`navbtn board ${view === "board" ? "active" : ""}`} href="#board-band" onClick={(e) => { e.preventDefault(); if (view === "front") { document.getElementById("board-band")?.scrollIntoView({ behavior: "smooth", block: "start" }); } else { setView("front"); setTimeout(() => document.getElementById("board-band")?.scrollIntoView({ behavior: "smooth", block: "start" }), 60); } }}>
|
||||
{boardCount > 0 && <span className="dot" />}Thank You Board
|
||||
</a>
|
||||
</nav>
|
||||
@ -246,6 +247,30 @@ export default function Home() {
|
||||
|
||||
{view === "front" && (
|
||||
<>
|
||||
{/* ── IN PICTURES — credited public photos, its own full-width band ── */}
|
||||
<section className="band photos-band">
|
||||
<div className="bandhead">
|
||||
<span className="bandtitle">In Pictures</span>
|
||||
<span className="bandsub">Public photos of the senator — every image credited to its source</span>
|
||||
</div>
|
||||
<div className="photoscroll">
|
||||
{PHOTOS.map((p, i) => (
|
||||
<figure className="photo" key={i} onClick={() => window.open(commonsUrl(p.commons), "_blank", "noopener")}>
|
||||
<div className="phimg" style={{ backgroundImage: `url(${imgSrc(p.url)})` }} />
|
||||
<figcaption>
|
||||
<div className="cap">{p.caption}</div>
|
||||
<div className="cred">📷 {p.source}</div>
|
||||
<div className="lic">{p.credit}</div>
|
||||
</figcaption>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
<div className="photos-note">
|
||||
Click a photo to view it on Wikimedia Commons. Photos are public-domain, sourced from the
|
||||
senator's official Senate / social pages. This section is separate from the news wire.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="cols">
|
||||
<div>
|
||||
{!edition && (
|
||||
@ -296,21 +321,6 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel railcard board-teaser">
|
||||
<div className="ph">Thank you, Senator BAM <span className="count">{boardCount} posted</span></div>
|
||||
{(boardPosts || []).slice(0, 4).map((p) => (
|
||||
<div className="post" key={p.id}>
|
||||
<div className="nick">{p.nick}</div>
|
||||
<div className="msg">{clipText(p.message, 120)}</div>
|
||||
<div className="ts">{ago(p.createdAt)}</div>
|
||||
</div>
|
||||
))}
|
||||
{(boardPosts || []).length === 0 && <div className="empty">No messages yet — be the first to say thank you.</div>}
|
||||
<a className="more" href="#board" onClick={(e) => { e.preventDefault(); setView("board"); window.scrollTo({ top: 0 }); }}>
|
||||
SIGN THE BOARD →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="panel railcard">
|
||||
<div className="ph">Who he is</div>
|
||||
<div style={{ padding: 14, color: "var(--muted)", fontSize: 15, fontWeight: 500, lineHeight: 1.5 }}>
|
||||
@ -319,11 +329,41 @@ export default function Home() {
|
||||
2025 <b style={{ color: "var(--gold)" }}>PalaPinas</b> esports championship, and
|
||||
a legislator who has made the fight for <b style={{ color: "var(--gold)" }}>college
|
||||
students, scholarships, and fair electricity</b> his signature. This desk follows
|
||||
the wire for every story that bears his name.
|
||||
the wire for every story that bears his name — from verified Philippine and
|
||||
international news outlets only.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── THE BOARD — its own prominent full-width band, not a news desk ── */}
|
||||
<section className="band board-band" id="board-band">
|
||||
<div className="bandhead">
|
||||
<span className="bandtitle">Thank you, Senator BAM</span>
|
||||
<span className="bandsub">The community board — say it to the senator, no account needed</span>
|
||||
</div>
|
||||
<form className="postform big" onSubmit={doPost}>
|
||||
<label htmlFor="bnick">Your nickname</label>
|
||||
<input id="bnick" value={nick} onChange={(e) => setNick(e.target.value)} maxLength={24} placeholder="e.g. PampangaProud, BamGang13" required />
|
||||
<label htmlFor="bmsg">Your message</label>
|
||||
<textarea id="bmsg" value={msg} onChange={(e) => setMsg(e.target.value)} maxLength={600} rows={3} placeholder="Thank you, Senator BAM — ..." required />
|
||||
<button className="go" type="submit" disabled={posting || !nick.trim() || !msg.trim()}>
|
||||
{posting ? "Sending..." : "Post to the board"}
|
||||
</button>
|
||||
{postErr && <div className="err">{postErr}</div>}
|
||||
</form>
|
||||
<div className="postlist board">
|
||||
{(boardPosts || []).slice(0, 12).map((p) => (
|
||||
<div key={p.id} className={`post ${p.id === freshId ? "fresh" : ""}`}>
|
||||
<div className="toprow"><span className="nick">{p.nick}</span><span className="ts">{ago(p.createdAt)}</span></div>
|
||||
<div className="msg">{p.message}</div>
|
||||
</div>
|
||||
))}
|
||||
{boardPosts && boardPosts.length === 0 && (
|
||||
<div className="empty">The board is empty. Say the first thank you.</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
11
lib/crawl.js
11
lib/crawl.js
@ -20,7 +20,7 @@
|
||||
import Parser from "rss-parser";
|
||||
import { getCollections } from "./db.js";
|
||||
import { sanitize, extractImage } from "./llm.js";
|
||||
import { BAM_QUERIES, PH_NATIVE, queryFeed, unwrapFeedLink, stripPublisherSuffix, publisherFrom, publisherFromUrl } from "./sources.js";
|
||||
import { BAM_QUERIES, PH_NATIVE, queryFeed, unwrapFeedLink, stripPublisherSuffix, publisherFrom, publisherFromUrl, publisherVerified } from "./sources.js";
|
||||
import { normalizeCategory } from "./categories.js";
|
||||
import { prefetchArticles } from "./article.js";
|
||||
|
||||
@ -193,7 +193,7 @@ export async function crawlOnce({ perFeed = 20, maxStories = 300, concurrency =
|
||||
// Normalise + gate (Pass 1).
|
||||
let candidates = [];
|
||||
const errors = [];
|
||||
let skippedStale = 0, skippedUndated = 0, skippedSynd = 0, gatedOut = 0;
|
||||
let skippedStale = 0, skippedUndated = 0, skippedSynd = 0, skippedUnverified = 0, gatedOut = 0;
|
||||
for (const r of results) {
|
||||
if (!r || r.error || !r.feed) {
|
||||
if (r && r.error) errors.push(`${r.job?.name || "job"}: ${r.error}`);
|
||||
@ -226,6 +226,10 @@ export async function crawlOnce({ perFeed = 20, maxStories = 300, concurrency =
|
||||
it.named = gate.named;
|
||||
return true;
|
||||
}).filter((it) => {
|
||||
// VERIFIED PUBLISHERS ONLY (Daniel, 2026-09-15): the article URL's host
|
||||
// must be on the allowlist. This runs before mirror/stale so a tabloid
|
||||
// story never reaches the desk even if it names him.
|
||||
if (!publisherVerified(it.link)) { skippedUnverified++; return false; }
|
||||
if (isNaN(it.published.getTime())) { skippedUndated++; return false; }
|
||||
if (isMirrorHost(it.link)) { skippedSynd++; return false; }
|
||||
if (it.published.getTime() < newestAllowed) { skippedStale++; return false; }
|
||||
@ -264,6 +268,7 @@ export async function crawlOnce({ perFeed = 20, maxStories = 300, concurrency =
|
||||
const gate = bamRelevance(it.title, it.desc, { requireNamed: false });
|
||||
if (!gate.ok) { gatedOut++; return false; }
|
||||
it.named = gate.named;
|
||||
if (!publisherVerified(it.link)) { skippedUnverified++; return false; }
|
||||
if (isNaN(it.published.getTime())) { skippedUndated++; return false; }
|
||||
if (isMirrorHost(it.link)) { skippedSynd++; return false; }
|
||||
if (it.published.getTime() < newestAllowed) { skippedStale++; return false; }
|
||||
@ -369,5 +374,5 @@ export async function crawlOnce({ perFeed = 20, maxStories = 300, concurrency =
|
||||
console.warn("[bama] article prefetch failed:", String((e && e.message) || e).slice(0, 120));
|
||||
}
|
||||
|
||||
return { added, reingested, withImage, gatedOut, skippedStale, skippedUndated, skippedSynd, named: namedCount, feeds: jobs.length, usedFallback, articles, errors: errors.slice(0, 12), collectedAt: now };
|
||||
return { added, reingested, withImage, gatedOut, skippedStale, skippedUndated, skippedSynd, skippedUnverified, named: namedCount, feeds: jobs.length, usedFallback, articles, errors: errors.slice(0, 12), collectedAt: now };
|
||||
}
|
||||
|
||||
75
lib/photos.js
Normal file
75
lib/photos.js
Normal file
@ -0,0 +1,75 @@
|
||||
// BidaAngMamamayan — curated public photos of Senator Bam Aquino.
|
||||
//
|
||||
// Every image is REAL and VERIFIED: fetched + license-checked via the
|
||||
// Wikimedia Commons API, and each URL confirmed to load as an image
|
||||
// (2006-09-15). Sources are the senator's official Senate / Facebook pages
|
||||
// (and one allied senator's page), mirrored on Wikimedia Commons, all
|
||||
// public domain. The site credits the original source on each card and
|
||||
// links out to the Commons file page — Daniel asked that we cite where
|
||||
// each photo came from.
|
||||
//
|
||||
// URL NOTE: we use the ORIGINALS on upload.wikimedia.org (not the
|
||||
// thumb.wikimedia.org scaled copies). The thumb host returns 502 to
|
||||
// datacenter egress under load; the originals are small (<320 KB each,
|
||||
// the portrait PNG is 1.6 MB) and load reliably. Do NOT scrape random
|
||||
// image hosts — only Commons-hosted, credited, public-domain photos.
|
||||
|
||||
const CDN = "https://commons.wikimedia.org/wiki/";
|
||||
|
||||
// url = verified image. caption/source/credit = the attribution shown on
|
||||
// the card. commons = the File: page (the "view source" link).
|
||||
export const PHOTOS = [
|
||||
{
|
||||
url: "https://upload.wikimedia.org/wikipedia/commons/4/4a/Bam_Aquino_2025_%28Cropped%29.png",
|
||||
caption: "The 2025 Senate portrait",
|
||||
source: "Senator Bam Aquino's official page",
|
||||
credit: "Public domain, via Wikimedia Commons",
|
||||
commons: "File:Bam_Aquino_2025_(Cropped).png",
|
||||
},
|
||||
{
|
||||
url: "https://upload.wikimedia.org/wikipedia/commons/7/71/Bam_Aquino_and_Pope_Francis.jpg",
|
||||
caption: "Meets Pope Francis at St. Peter's Square, Vatican",
|
||||
source: "Senator Bam Aquino's official Facebook page",
|
||||
credit: "Public domain, via Wikimedia Commons",
|
||||
commons: "File:Bam_Aquino_and_Pope_Francis.jpg",
|
||||
},
|
||||
{
|
||||
url: "https://upload.wikimedia.org/wikipedia/commons/5/5c/Senator_Bam_Aquino_and_Education_Secretary_Sonny_Angara.jpg",
|
||||
caption: "With DepEd Sec. Sonny Angara on the classroom / free-college push",
|
||||
source: "Senator Bam Aquino's official Facebook page",
|
||||
credit: "Public domain, via Wikimedia Commons",
|
||||
commons: "File:Senator_Bam_Aquino_and_Education_Secretary_Sonny_Angara.jpg",
|
||||
},
|
||||
{
|
||||
url: "https://upload.wikimedia.org/wikipedia/commons/4/4e/Senator_Bam_Aquino_with_Bini_%2801%29.jpg",
|
||||
caption: "Nation's girl group BINI visits the Philippine Senate",
|
||||
source: "Senator Bam Aquino's official Facebook page",
|
||||
credit: "Public domain, via Wikimedia Commons",
|
||||
commons: "File:Senator_Bam_Aquino_with_Bini_(01).jpg",
|
||||
},
|
||||
{
|
||||
url: "https://upload.wikimedia.org/wikipedia/commons/1/12/Bam_Aquino%2C_Brent_Manalo_and_Mika_Salamanca_in_2025.jpg",
|
||||
caption: "With PBB: Celebrity Collab winners Brent Manalo & Mika Salamanca",
|
||||
source: "Senator Bam Aquino's official Facebook page",
|
||||
credit: "Public domain, via Wikimedia Commons",
|
||||
commons: "File:Bam_Aquino,_Brent_Manalo_and_Mika_Salamanca_in_2025.jpg",
|
||||
},
|
||||
{
|
||||
url: "https://upload.wikimedia.org/wikipedia/commons/f/f5/Risa_Hontiveros%2C_Bam_Aquino%2C_and_Kiko_Pangilinan_in_2025.jpg",
|
||||
caption: "At a 2025 campaign event with Sen. Risa Hontiveros & Sen. Kiko Pangilinan",
|
||||
source: "Senator Risa Hontiveros's official Facebook page",
|
||||
credit: "Public domain, via Wikimedia Commons",
|
||||
commons: "File:Risa_Hontiveros,_Bam_Aquino,_and_Kiko_Pangilinan_in_2025.jpg",
|
||||
},
|
||||
{
|
||||
url: "https://upload.wikimedia.org/wikipedia/commons/6/60/Senator_Bam_Aquino_at_the_Ninoy_Aquino_International_Airport_Terminal_1_to_commemorate_the_42nd_anniversary_of_the_assassination_of_Ninoy_Aquino_%2801%29.jpg",
|
||||
caption: "NAIA Terminal 1 — 42nd anniversary of Ninoy Aquino's assassination",
|
||||
source: "Senator Bam Aquino's official Facebook page",
|
||||
credit: "Public domain, via Wikimedia Commons",
|
||||
commons: "File:Senator_Bam_Aquino_at_the_Ninoy_Aquino_International_Airport_Terminal_1_to_commemorate_the_42nd_anniversary_of_the_assassination_of_Ninoy_Aquino_(01).jpg",
|
||||
},
|
||||
];
|
||||
|
||||
export function commonsUrl(file) {
|
||||
return CDN + encodeURIComponent(file).replace(/%20/g, "_");
|
||||
}
|
||||
@ -35,14 +35,78 @@ export const BAM_QUERIES = [
|
||||
{ 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. The crawler runs these ONLY when the BAM queries produced
|
||||
// zero named stories this tick (verified: that happens on quiet days). Their
|
||||
// items pass through the same gate, so only genuine senate/BAM-adjacent copy
|
||||
// ever prints from here.
|
||||
// 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 ────────────────────────────────────────────────────────
|
||||
|
||||
@ -25,7 +25,7 @@ let run = async () => {
|
||||
const j = await res.json().catch(() => ({}));
|
||||
const c = (j && j.crawl) || {};
|
||||
console.log(
|
||||
`[bama-crawl] ${res.status} added=${c.added} named=${c.named} fallback=${c.usedFallback ? "yes" : "no"} gated=${c.gatedOut} imgs=${c.withImage} articles=${c.articles ? c.articles.ok + "/" + c.articles.fetched : "?"} ${c.errors && c.errors.length ? "errs=" + c.errors.length : ""}`
|
||||
`[bama-crawl] ${res.status} added=${c.added} named=${c.named} fallback=${c.usedFallback ? "yes" : "no"} gated=${c.gatedOut} unverified=${c.skippedUnverified} imgs=${c.withImage} articles=${c.articles ? c.articles.ok + "/" + c.articles.fetched : "?"} ${c.errors && c.errors.length ? "errs=" + c.errors.length : ""}`
|
||||
);
|
||||
process.exit(res.ok ? 0 : 1);
|
||||
};
|
||||
|
||||
@ -107,18 +107,35 @@ if (await lead.count()) {
|
||||
errors.push("no lead story card");
|
||||
}
|
||||
|
||||
// board view — the nav tab (there's also a "SIGN THE BOARD" link on the front)
|
||||
await page.getByRole("link", { name: "Thank You Board" }).click();
|
||||
await page.waitForTimeout(800);
|
||||
await page.screenshot({ path: OUT + "/03-board-empty.png" });
|
||||
await page.fill("#nick", "PampangaGamer");
|
||||
await page.fill("#msg", "Thank you Senator BAM for the scholarship expansion — my whole batch is riding on it!");
|
||||
await page.locator("button.go").click();
|
||||
// In Pictures band — credited photos, its own area (not a desk)
|
||||
const photosBand = page.locator(".photos-band");
|
||||
if (await photosBand.count()) {
|
||||
const photos = await photosBand.locator(".photo").count();
|
||||
if (photos < 5) errors.push("expected >=5 credited photos, got " + photos);
|
||||
const credText = await photosBand.textContent();
|
||||
if (!/Public domain/i.test(credText)) errors.push("photo source credit missing");
|
||||
if (!/official/i.test(credText)) errors.push("photo 'official' source label missing");
|
||||
await photosBand.screenshot({ path: OUT + "/03a-in-pictures.png" }).catch(() => {});
|
||||
} else {
|
||||
errors.push("In Pictures band missing");
|
||||
}
|
||||
|
||||
// the front-page BOARD band (its own area, not a desk) — post a thank-you
|
||||
await page.locator("#board-band").scrollIntoViewIfNeeded().catch(() => {});
|
||||
await page.waitForTimeout(300);
|
||||
const boardBand = page.locator("#board-band");
|
||||
if (await boardBand.count()) {
|
||||
await page.fill("#bnick", "PampangaGamer");
|
||||
await page.fill("#bmsg", "Thank you Senator BAM for the scholarship expansion — my whole batch is riding on it!");
|
||||
await boardBand.locator("button.go").click();
|
||||
await page.waitForTimeout(1200);
|
||||
await page.screenshot({ path: OUT + "/04-board-posted.png" });
|
||||
const boardText = await page.textContent(".postlist");
|
||||
if (!/PampangaGamer/.test(boardText || "")) errors.push("posted nick not rendered");
|
||||
if (!/Thank you Senator BAM for the scholarship/.test(boardText || "")) errors.push("posted message not rendered");
|
||||
await boardBand.screenshot({ path: OUT + "/04-board-posted.png" }).catch(() => {});
|
||||
const boardText = await boardBand.textContent();
|
||||
if (!/PampangaGamer/.test(boardText || "")) errors.push("board band: posted nick not rendered");
|
||||
if (!/Thank you Senator BAM for the scholarship/.test(boardText || "")) errors.push("board band: posted message not rendered");
|
||||
} else {
|
||||
errors.push("front-page board band missing");
|
||||
}
|
||||
|
||||
// back to front, then a desk view — click the Education desk card (grid lives on front)
|
||||
await page.getByRole("link", { name: "Front", exact: true }).click();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user