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.
42 lines
1.6 KiB
JavaScript
42 lines
1.6 KiB
JavaScript
// Text utilities kept from forthena's lib/llm.js (this site has NO LLM layer —
|
|
// the ranking is deterministic, see lib/feed.js). The file keeps the llm.js
|
|
// module name so the crawl/article imports don't churn.
|
|
|
|
export function sanitize(s) {
|
|
return String(s || "")
|
|
.replace(/<[^>]*>/g, " ")
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
// First image an RSS item exposes, in descending order of reliability.
|
|
export function extractImage(item, feed) {
|
|
const cands = [];
|
|
const enc = item.enclosure;
|
|
if (enc && enc.url && (/image/i.test(enc.type || "") || /\.(jpe?g|png|webp|gif|avif)/i.test(enc.url))) cands.push(enc.url);
|
|
for (const key of ["mediaContent", "mediaThumbnail", "media:content", "media:thumbnail"]) {
|
|
const v = item[key];
|
|
const list = Array.isArray(v) ? v : v ? [v] : [];
|
|
for (const m of list) {
|
|
const u = (m && (m["$"]?.url || m.url || m.$?.url)) || null;
|
|
if (u) cands.push(u);
|
|
}
|
|
}
|
|
const itn = item.itunesImage || item.image;
|
|
if (itn) cands.push((itn["$"] && itn["$"].href) || itn.href || (typeof itn === "string" ? itn : null));
|
|
const html = [item["content:encoded"], item.content, item.contentSnippet, item.summary]
|
|
.filter(Boolean)
|
|
.join(" ");
|
|
const m = /<img[^>]+src=["']([^"']+)["']/i.exec(html);
|
|
if (m) cands.push(m[1]);
|
|
const fi = feed && feed.image && (feed.image.url || feed.image.$?.url);
|
|
if (fi) cands.push(fi);
|
|
for (const u of cands) {
|
|
const s = String(u || "").trim();
|
|
if (!/^https?:\/\//i.test(s)) continue;
|
|
if (/1x1|spacer|pixel\.gif|blank\.|logo-|sprite/i.test(s)) continue;
|
|
return s.slice(0, 500);
|
|
}
|
|
return null;
|
|
}
|