-
Step 1 · Pick sources and parse the feeds
Settle on 10–20 sources, then normalise them into one JSON
The value of an RSS reader isn't "it reads everything". It's you already filtered the list for the reader.
Choosing sources:
- Personal blogs and indie developers first — better quality, steadier updates, less marketing
- Look at each source's last 10 posts; if more than half are promo, drop it
- Start with 10–20 sources. More than that and you won't read it yourself
One unified format — everything downstream depends on it:
{ "updated": "2026-09-27T08:00:00Z", "items": [ { "title": "...", "url": "...", "source": "source name", "published": "2026-09-27", "summary": "..." } ] }Parsing:
fast-xml-parserorrss-parserin Node. There are three dialects — RSS 2.0, Atom and RDF — so don't hand-roll a regex. Use a library.Fallbacks for missing fields: no
summary? Take the first 120 characters of the body. Nopublished? Use the fetch time. One dirty record must not break the whole list.Checklist
- At least 10 sources, all parsed successfully
- All items normalised into the same JSON structure
- Sources with missing fields don't cause errors
-
Step 2 · Fetch on a schedule, land it, dedupe
Hourly crontab, dedupe by url, keep only the last N items
The fetch script does three things: fetch → merge → write.
# crontab: run at minute 7 of every hour 7 * * * * /usr/bin/node /root/rss/fetch.js >> /root/rss/fetch.log 2>&1Merge rules — these decide whether the list looks good:
- Dedupe by
url: when several sources carry the same article, keep one (the earliest publisher wins) - Sort by
publisheddescending — newest first - Keep only the last 200–500 items: unbounded growth makes the JSON big and the page slow
- Write incrementally: read the old JSON, merge, then write. Don't recompute everything each run
Failure handling: if one source fails, log it and skip it. Never abort the whole batch. Flag any source that fails three runs in a row — go check whether it's dead.
Where it lands:
/www/wwwroot/yourdomain/data/feed.json, with the JSON MIME type set in Nginx (seescripts/nginx-site.conf), so the front end can fetch it directly.And don't forget: run it through a JSON linter. A blank page is a broken JSON file about 90% of the time.
Checklist
- The fetch task is in crontab and the log has output
- Duplicate articles appear only once
- feed.json stays within the expected size
- Dedupe by
-
Step 3 · Front end: list + filtering + read state
Filter by source and keyword; read state lives in localStorage
A reader page is three pieces: source filter / list / read state.
Rendering the list:
- Each row shows title, source name, date and a one-line summary; the title opens the original in a new tab
- Filter by source — a row of chips at the top — and by keyword, an input matching title and summary locally
- Read items go grey: store the
urlinlocalStoragewhen clicked, and it stays grey after a reload
// Read state: store a set of urls, not the whole feed const read = new Set(JSON.parse(localStorage.getItem('read') || '[]')); // on click: read.add(url) and write it backThe static-site trade-off: read state is local, so it doesn't sync across devices. That's a feature, not a bug — say so on the page, and don't add a backend just to sync it.
Performance: under 500 items, render everything; no pagination needed. Above that, split by date behind a "load more".
Fallback: if the fetch fails, show "data unavailable right now" plus the last update time. Never a blank page.
Checklist
- Filtering by source and by keyword works
- Read state survives a reload
- A failed fetch shows fallback copy
-
Step 4 · Launch + indexing observation + long-term maintenance
The page needs to be findable, and the source list needs pruning
Launch: upload the static files, or host a subdirectory such as
reader.yourdomain; request an HTTPS certificate; write the sitemap and submit it.Indexing observation — it matters more for a feed site:
A feed page changes every day, so what a crawler saw and what you display now may not match.- Write the static part of the page to say what this site is, which sources it carries and how often it updates. That's the part a crawler reads
- Include the reader page in
sitemap.xml, withlastmodmoving whenever the data does - Put the URL in
data/index-observation.jsonand run the observation script on a schedule - Give the source list its own static page (
/sources.html) — that page is your real SEO asset, because its content is stable and its keywords are unambiguous
Maintenance (20 minutes a month):
- Drop sources that haven't updated in 30 days or have gone downhill; add 2–3 new ones
- Watch page views and clicks: whichever source's articles get clicked most is the direction to add more sources in
- Instrument
feed_click(clicking an item through to the original) as the primary conversion event, and register it indata/conversion.json
Don't: generate a pile of empty aggregation pages in the hope of getting indexed. That dilutes the site rather than adding coverage.
Checklist
- The source list is a standalone static page, submitted for indexing
- feed_click is queryable
- There's a monthly record of sources dropped and added