I built a live news aggregator in a weekend no backend, no database, free forever
Every morning I had the same problem: 15 browser tabs open just to read the news.
BBC for world news. TechCrunch for tech. Guardian for science. Al Jazeera for a different perspective. ESPN for sports. It was exhausting — and each one came with cookies banners, account prompts, paywalls and autoplay videos.
So I built The Brief — a free live news aggregator that pulls from 20+ sources in one clean interface. No account. No tracking. No ads (yet). Just news.
Here's exactly how I built it, what went wrong, and what surprised me.
The stack
- React 18 + Vite — frontend SPA
- Netlify — hosting + serverless functions
- Own RSS proxy — replaced a third-party service (more on this below)
- Open-Meteo API — free weather widget
- localStorage — 3-layer cache system
No database. No auth. No backend server. Total monthly cost: £0.
The RSS problem
My first version used rss2json.com to convert RSS feeds to JSON. It worked great — until it didn't. Rate limits, downtime, and a free tier that wasn't actually free at scale.
So I wrote my own Netlify function:
// netlify/functions/rss.js
export async function handler(event) {
const url = event.queryStringParameters.url;
const res = await fetch(url);
const xml = await res.text();
// Parse RSS + Atom feeds, handle CDATA, namespaced tags
const items = parseXml(xml);
return {
statusCode: 200,
headers: {
'Cache-Control': 'public, max-age=600',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({ items })
};
}
Zero rate limits. Zero third-party dependency. Netlify gives you 125,000 function calls per month on the free tier. At current traffic I'm using about 3%.
The 10-minute CDN cache header means even if 1,000 people load the site simultaneously, BBC's RSS server only gets hit once per 10 minutes.
The 3-layer cache
First load is slow (fetching 3+ RSS feeds per category). Return visits should be instant. Here's how I solved it:
// Layer 1 — in-memory (survives tab switches, dies on refresh)
const cacheRef = useRef({});
// Layer 2 — localStorage with 1hr TTL (survives refresh)
function saveCache(key, data) {
localStorage.setItem(`theBriefCache_${key}`, JSON.stringify({
data,
ts: Date.now()
}));
}
function loadCache(key) {
const raw = localStorage.getItem(`theBriefCache_${key}`);
if (!raw) return null;
const { data, ts } = JSON.parse(raw);
if (Date.now() - ts > 3600000) return null; // 1hr TTL
return data;
}
// Layer 3 — fetch from Netlify function
async function fetchFeed(url) {
const res = await fetch(`/.netlify/functions/rss?url=${encodeURIComponent(url)}`);
return res.json();
}
The flow: memory → localStorage → network. Most return visits never make a single network request.
Filtering — the hardest part
Pulling from generic RSS feeds means getting off-topic content everywhere. BBC Sport's feed includes rugby, cricket, athletics — but if someone clicks "NFL" they only want American football.
I built a keyword filter system:
const CATEGORY_FILTERS = {
nfl: {
require: [
"nfl", "american football", "quarterback", "touchdown",
"kansas city chiefs", "philadelphia eagles", "patrick mahomes",
// ... 30+ more specific terms
]
},
climbing: {
require: [
"climbing", "bouldering", "lead climbing", "free solo",
"ifsc", "v-grade", "8a", "9a", "adam ondra", "magnus midtbø",
// ... climbing-specific terms only
]
}
};
function passesFilter(article, category) {
const filter = CATEGORY_FILTERS[category];
if (!filter) return true;
const text = (
(article.title || "") + " " +
(article.description || "") + " " +
(article.source || "")
).toLowerCase();
return filter.require.some(kw => text.includes(kw));
}
The same filter applies to YouTube videos — which fixed the most embarrassing bug: "Chrome for Developers" appearing in the NBA section because ESPN's YouTube channel posts everything.
Lesson learned: never trust that a sports channel only posts sports content.
The YouTube integration
YouTube has a free RSS feed for every channel that nobody talks about:
https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID
No API key. No quota. Same endpoint every podcast app and RSS reader uses. I fetch these server-side through the same Netlify function pattern and inject videos between articles in the feed.
// Videos appear every 4th article
if ((i + 1) % 4 === 0 && vIdx < filteredVideos.length) {
mixed.push({ ...filteredVideos[vIdx], _type: "video" });
vIdx++;
}
The share system
Articles don't have individual URLs — it's a SPA. When someone shares an article, I needed a way for the recipient to land inside the app with the right article open.
Solution: encode the article URL as base64 in a ?a= query parameter, store metadata in sessionStorage for same-device shares, and decode on load:
// Sharing
const key = btoa(unescape(encodeURIComponent(articleUrl)))
.replace(/[+/=]/g, c => ({"+":"_","/":"-","=":""})[c]);
sessionStorage.setItem("tb_" + key.slice(0,12), JSON.stringify({
title, description, source, image, url: articleUrl
}));
const shareUrl = `https://thebriefnews.org/?a=${key}`;
// On load — decode for any device
const b64 = key.replace(/_/g,"+").replace(/-/g,"/");
const decoded = decodeURIComponent(escape(atob(b64 + "==")));
// decoded = original article URL
What surprised me
1. RSS is alive and well. Every major publisher still maintains RSS feeds. BBC, NYT, Guardian, Al Jazeera, TechCrunch — all publishing clean, well-structured feeds updated every few minutes. It's underrated.
2. The 1-hour cache is the whole product. Without caching, the site would be unusably slow. With it, most visits are instant. The UX difference is night and day.
3. Keyword filtering is harder than it looks. "Bears" in a headline could be Chicago Bears or actual bears. "Patriots" could be NFL or political. Full team names (chicago bears, new england patriots) work much better than single words.
4. People share news differently than I expected. The most common share path is WhatsApp — not Twitter. Building a proper OG image and share URL system mattered more than I thought.
The numbers — one week in
- 1,497 pageviews, 524 unique visitors in the first week
- Launched on Product Hunt — got featured
- Google indexed within 4 days
- 22.2% CTR from Google search results (industry average is 2-5%)
- Traffic from: USA (42%), Spain (26%), France, Germany, India
What's next
- Server-side caching with Upstash Redis when traffic grows
- Web Push notifications for breaking news
- TypeScript migration
- More source categories
Links
- 🌐 Live: thebriefnews.org
- 💻 GitHub: github.com/esteves7771/the-brief
Happy to answer any questions about the architecture. The whole thing is open source — feel free to poke around the code.
Built by Pedro Esteves — frontend developer based in Barcelona.
Top comments (0)