DEV Community

Pedro Esteves
Pedro Esteves

Posted on

I built a live news aggregator in a weekend no backend, no database, free forever

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 })
  };
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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++;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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)