bidaangmamamayan/lib/categories.js
Ryan 6b06e08c08 BidaAngMamayan: public supporter tribute news desk for Senator BAM Aquino
Forked from Forthena (PERSONAL/forthena) and retargeted:
- NO login, NO LLM: the edition is public and the ranking is deterministic
  (stories that name Sen. BAM lead; freshest first) — Daniel: no
  like/dislike or store profile for visitors.
- Crawler scoped to BAM: Bing news keyword queries + Inquirer/Rappler
  fallback wires, with a two-tier relevance gate (named-BAM always;
  Aquino+legislative-context only from fallback) so op-eds and non-Bam
  senators are dropped.
- Desks: BAM (lead), Gaming, Education, Politics, Energy, PH, Business.
- Community board /board: 'Thank you Senator BAM' — nickname-only posts,
  HTML-stripped, URL-only rejected, IP rate-limited (3/10min, 20/day).
- Yellow-gamer theme (Orbitron/Rajdhani/JetBrains Mono, dark + gold,
  HUD corners), supporter disclaimer in header and footer.
- Tests: category/date/relevance/text suites + Mongo board suite +
  full HTTP e2e (all passing).

Infra: 66.181.46.132 (bidaangmamamayan.com), local Mongo 7.0,
nginx + certbot TLS, PM2 bama (:3000) + 15-min crawl cron.
2026-09-15 08:37:03 +00:00

52 lines
2.8 KiB
JavaScript

// BidaAngMamamayan desk registry — single source of truth for categories.
//
// A tribute site has fewer desks than a general paper, but each one is
// deliberate: the "BAM" lead desk, his signature beats (education/college,
// energy/electricity, gaming/esports, the Senate floor), and the PH + world
// context he works inside.
export const CATEGORIES = [
{ key: "bam", label: "BAM Front", blurb: "Every story that names him" },
{ key: "education", label: "Education", blurb: "College, scholarship, the classroom" },
{ key: "energy", label: "Energy & Electricity", blurb: "The power bill fight" },
{ key: "gaming", label: "Gamer's Corner", blurb: "Esports, gaming, the 2025 runner-up" },
{ key: "politics", label: "The Senate Floor", blurb: "Bills, sessions, the chamber" },
{ key: "ph", label: "The Philippines", blurb: "Home context, region by region" },
{ key: "world", label: "World", blurb: "The rest of the wire" },
];
export const CATEGORY_KEYS = new Set(CATEGORIES.map((c) => c.key));
export const CATEGORY_LABELS = Object.fromEntries(CATEGORIES.map((c) => [c.key, c.label]));
// Anything that isn't a valid desk key falls back to the lead desk. The gate
// (lib/crawl.js) already decided the story belongs on this site, so "bam" is
// the honest default, not "world".
export function normalizeCategory(c) {
if (typeof c === "string" && CATEGORY_KEYS.has(c)) return c;
return "bam";
}
export function sectionName(c) {
return CATEGORY_LABELS[normalizeCategory(c)] || "BAM Front";
}
// The relevance gate lives in lib/crawl.js (it needs the description text);
// this is just the desk classifier for rows that pass the gate but arrive
// without a query tag.
const CLASSIFIERS = [
["gaming", /\besports?\b|\bgaming\b|\bvideo games?\b|\bsteam\b|\bplaystation\b|\bnintendo\b|\bxbox\b|\bconsole\b|\bgame (studio|developer|release)\b|\briot\b|\bvalorant\b|\blol\b|\bdota\b/i],
["education", /\beducation\b|\bschool(s|ing)?\b|\bcollege\b|\buniversity\b|\bscholarship\b|\bstudents?\b|\bclassroom\b|\bteachers?\b|\blearning\b|\bched\b|\bdeped\b/i],
["energy", /\bpower (outage|bill|rates?|grid)\b|\belectricity\b|\bbrownout\b|\bblackout\b|\bvat.*\bpower\b|\benergy\b|\bsystem loss\b|\belectric rate\b/i],
["politics", /\bsenate\b|\bbill\b|\bcongress\b|\bpresidenc?\b|\belection\b|\bparty\b|\bpolitical\b|\bimpeach\b|\bcommitte\w*\b/i],
["ph", /\bphilippines?\b|\bmanila\b|\bmarikina\b|\bquezon city\b|\bpampanga\b|\bc[eé]bu city\b|\bdavao\b|\bbaguio\b|\bpalawan\b|\bmindanao\b|\bvisayas\b|\bluzon\b|\bcalabarzon\b|\bbarangay\b|\bsenior citizen\b|\bprovincial\b|\bpeso\b/i],
];
export function classifyStory({ title, summary, source } = {}) {
const text = `${title || ""} ${summary || ""} ${source || ""}`;
if (!text.trim()) return "bam";
for (const [key, re] of CLASSIFIERS) {
if (re.test(text)) return key;
}
return "bam";
}