Syndicator Is a Zero-Dependency RSS Aggregator Built With One Node Command
The front page of syndicator.net is a river of headlines from 114 sites, newest first. Behind it there's no database, no server process and no framework. There is one command:
node build.mjs
It reads a list of feeds, fetches them, merges the newest items into one stream, and writes a folder of static files. Then any static host serves the folder. That's the whole design.
What it produces
One build writes:
- A paginated home page, ten articles per page, each with the source site, a title linking to the original article, a date and a short excerpt.
- A /sites/ page listing every source with its home page and feed.
- An Atom feed of the merged stream at /feed.xml, so you can follow all 114 sites in a reader with one subscription.
- An OPML file with each feed as a separate subscription, for people who'd rather follow them one by one.
- A sitemap, robots.txt, a web manifest, a 404 page and a few static pages like About and Contact.
Every article links out. Syndicator shows a headline and roughly 200 characters of summary. The reading happens on the original site.
Zero dependencies, for real
There's no node_modules folder, no lockfile, no install step. It needs Node 18 or newer, because Node 18 is where fetch became built in. Everything else is the standard library.
That includes the feed parsing. Instead of pulling in an XML library, Syndicator has a small tolerant extractor that finds item and entry blocks and reads named children out of them. It copes with CDATA, namespace prefixes, attributes and self-closing tags. It's deliberately not a general XML parser. It handles RSS 2.0, RDF and Atom, plus JSON Feed through JSON.parse, and it's covered by tests against sample WordPress, Hugo, Atom, JSON Feed and deliberately broken documents.
Why go to that trouble? A build that has no dependencies can't break when one of them changes. It installs instantly on any CI or static host. And the whole codebase is small enough to read in an afternoon, about 1,200 lines including the HTML templates.
Configuration in two files
feeds.json is the list. Each entry has four fields:
{
"id": "pxef",
"name": "Pixel Effects",
"url": "https://pxef.com/",
"feed": "https://pxef.com/feed/"
}
Optional flags hide a source from the sites page, mark it as JSON Feed, or switch it off.
config.mjs holds everything else: site title, navigation, how many items to take per feed, page size, excerpt length, timeouts and cache settings. Nothing else needs editing to rebrand it.
The build checks feeds.json before it touches the network. A missing field, a malformed URL or a duplicate id, name or feed URL stops the build with a clear message. Those are exactly the mistakes that otherwise drop a site out of the stream with nobody noticing.
Three modes
node build.mjs # fetch everything, build into _site/
node build.mjs --offline # rebuild from the cache, no network
node build.mjs --serve # build, serve on :8080, rebuild on change
The offline mode is for working on templates without hammering 114 servers every time you change a color.
Why build it at all
There are plenty of feed readers and some aggregators. Most want an account, a database or a server running all the time. A static aggregator is a different kind of thing. It's a public page, cheap enough to host for free, that turns a group of related sites into one front door, with its own feed for people who want to follow the whole group.
How it deals with feeds that break is the part that took the most thought. That's in the next post. How it stays current without a server is here.